<template>
  <div
    class="waterfall"
    ref="waterfallRef"
    :style="{ height: waterfallHeight ? waterfallHeight + 'px' : '100px' }"
  >
    <template v-if="data.length && columnWidth">
      <div
        class="waterfall-item"
        v-for="(item, index) in data"
        :key="nodeKey ? item[nodeKey] : index"
        :style="{
          width: columnWidth + 'px',
          left: item._style?.left + 'px',
          top: item._style?.top + 'px'
        }"
      >
        <slot :item="item" :width="columnWidth" :index="index" />
      </div>
    </template>
    <div v-else style="font-size: 0.2rem">loading...</div>
  </div>
</template>

<script setup>
import { ref, onMounted, computed, watch, nextTick, onUnmounted } from 'vue'
import {
  getImgElements,
  getAllImg,
  onCompileImgs,
  getMinHeightColumn,
  getMinHeight,
  getMaxHeight
} from './utils'
import { isMobileTerminal } from '@/utils/flexible'

const props = defineProps({
  data: Array,
  nodeKey: {
    type: String
  },
  column: {
    default: 2,
    type: Number
  },
  // 列间距
  columnSpacing: {
    default: 20,
    type: Number
  },
  // 行间距
  rowSpacing: {
    default: 20,
    type: Number
  },
  // 是否需要进行图片预读取
  picturePreReading: {
    type: Boolean,
    default: true
  }
})

const waterfallRef = ref(null)

// 容器总高度
const waterfallHeight = ref(0)

/**
 * 记录每列高度
 */
const columnHeightObj = ref({})
const useColumnHeightObj = () => {
  // key:所在列 val：高度
  columnHeightObj.value = {}
  // 初始化
  for (let i = 0; i < props.column; i++) {
    columnHeightObj.value[i] = 0
  }
}

// 容器左边距
const waterfallLeft = ref(0)
// 容器总宽度
const waterfallWidth = ref(0)
/**
 * 计算容器总宽度
 */
const useWaterfallWidth = () => {
  const waterfallDom = waterfallRef.value
  const { paddingLeft, paddingRight } = getComputedStyle(waterfallDom)
  waterfallWidth.value =
    waterfallDom.offsetWidth -
    parseFloat(paddingLeft) -
    parseFloat(paddingRight)
}

// 列宽
const columnWidth = ref(0)
//列间距合计
const columnSpacingTotal = computed(() => {
  return (props.column - 1) * props.columnSpacing
})
/**
 * 计算列宽
 */
const useColumnWidth = () => {
  // 计算容器宽
  useWaterfallWidth()
  columnWidth.value =
    (waterfallWidth.value - columnSpacingTotal.value) / props.column
}

onMounted(() => {
  useColumnWidth()
})

// item 高度集合
let itemHeights = []
/**
 * 监听图片加载完成(图片预加载)
 */
const waitImgCompile = () => {
  itemHeights = []
  // 拿到所有元素
  let itemElements = [...document.getElementsByClassName('waterfall-item')]
  // 获取所有元素img标签
  const imgElements = getImgElements(itemElements)

  // 获取所有img标签src
  const allImgs = getAllImg(imgElements)
  // 等待图片加载完成
  onCompileImgs(allImgs).then(() => {
    itemElements.forEach((el) => {
      itemHeights.push(el.offsetHeight)
    })
    // 渲染位置
    useItemLocation()
  })
}

/**
 * 不需要图片预加载
 */
const useItemHeight = () => {
  itemHeights = []
  // 拿到所有元素
  let itemElements = [...document.getElementsByClassName('waterfall-item')]
  // 计算高度
  itemElements.forEach((el) => {
    itemHeights.push(el.offsetHeight)
  })
  // 渲染位置
  useItemLocation()
}

/**
 * 渲染位置
 */
const useItemLocation = () => {
  //  遍历数据源
  props.data.forEach((item, index) => {
    // 避免重复计算
    if (item._style) return
    item._style = {}
    item._style.left = getItemLeft()
    item._style.top = getItemTop()
    // 指定的列高度自增
    increaseHeight(index)
  })
  // 指定容器的高度
  waterfallHeight.value = getMaxHeight(columnHeightObj.value)
}

/**
 * 组件销毁，清除所有_style
 */
onUnmounted(() => {
  props.data.forEach((item) => {
    delete item._style
  })
})

/**
 * 返回下一个item 的 left
 */
const getItemLeft = () => {
  // 拿到最小高度的列
  const column = getMinHeightColumn(columnHeightObj.value)
  return (
    column * (columnWidth.value + props.columnSpacing) + waterfallLeft.value
  )
}

/**
 * 返回下一个item 的 top
 */
const getItemTop = () => {
  return getMinHeight(columnHeightObj.value)
}

/**
 * 指定列高度自增
 */
const increaseHeight = (index) => {
  // 最小高度的列
  const minHeightColumn = getMinHeightColumn(columnHeightObj.value)
  // 开始自增
  columnHeightObj.value[minHeightColumn] +=
    itemHeights[index] + props.rowSpacing
}

watch(
  () => props.data,
  async (newVal) => {
    // 第一次获取数据时，构建高度记录容器
    const resetColumnHeight = newVal.every((item) => !item._style)
    if (resetColumnHeight) {
      useColumnHeightObj()
    }
    await nextTick()
    if (props.picturePreReading) {
      waitImgCompile()
    } else {
      useItemHeight()
    }
  },
  {
    deep: true,
    immediate: true
  }
)

/**
 * 监听列数变化，重新构建瀑布流
 */
const reset = () => {
  setTimeout(() => {
    // 重新计算列宽
    useColumnWidth()
    // 重置所有的定位数据，因为 data 中进行了深度监听，所以该操作会触发 data 的 watch
    props.data.forEach((item) => {
      item._style = null
    })
  }, 100)
}
watch(
  () => props.column,
  () => {
    if (props.picturePreReading) {
      // 在 picturePreReading 为 true 的前提下，需要首先为列宽滞空，列宽滞空之后，会取消瀑布流渲染
      columnWidth.value = 0
      // 等待页面渲染之后，重新执行计算。否则在 item 没有指定过高度的前提下，计算出的 item 高度会不正确
      nextTick(reset)
    } else {
      reset()
    }
  }
)

const observerPCWidth = () => {
  if (!isMobileTerminal.value && window.innerWidth < 500) return
  reset()
}

window.addEventListener('resize', observerPCWidth)
</script>

<style lang="scss" scoped>
.waterfall {
  position: relative;
  .waterfall-item {
    position: absolute;
  }
}
</style>
